package it.aqquadro.arex;

import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.logging.Logger;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;

import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.api.taskqueue.TaskOptions.Method;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;

public abstract class Arex implements ArexMouth {

	private static final Logger log = Logger.getLogger(Arex.class.getName());

	protected static final String CACHE_CONTROL_HEADER = "Cache-Control";

	protected static final String UTF8_CHARACTER_ENCODING = "UTF-8";

	protected static final String JSONP_CALLBACK_PARAMETER = "callback";

	protected static final String JSON_CONTENT_TYPE = "application/json";

	protected static final String JSONP_CONTENT_TYPE = "application/javascript";

	protected static final String CACHE_CONTROL_HEADER_VALUE = "no-store, no-cache, must-revalidate";

	protected static final String ROOT_PATH_CONTEXT_PARAM = "rootPath";

	private static final String ACCEPT_HEADER = "Accept";

	protected final Gson gson = new Gson();

	private HttpServletRequest req = null;

	private HttpServletResponse resp = null;

	private ServletContext servletContext;

	public Arex() throws IOException {
	}

	/**
	 * fare l'override di questo se si vuole cambiare l'encoding di una specifica richiesta
	 * 
	 * @return
	 */
	protected String getCharacterEncoding() {
		return UTF8_CHARACTER_ENCODING;
	}

	/**
	 * fare l'override di questo metodo se si vuole cambiare il parametro della callback jsonp di una specifica richiesta
	 * 
	 * @return
	 */
	protected String getJSONPCallback() {
		return JSONP_CALLBACK_PARAMETER;
	}

	/**
	 * fare l'ovveride di questo per gestire la messa in cache o meno
	 * 
	 * @return
	 */
	protected boolean isCached() {
		return false;
	}

	/**
	 * REST method
	 * 
	 * @return
	 * @throws IOException
	 */
	public Object head() throws IOException, ServletException {
		return notImplemented("NOT IMPLEMENTED");
	}

	/**
	 * REST method
	 * 
	 * @return
	 * @throws IOException
	 */
	public Object options() throws IOException, ServletException {
		return notImplemented("NOT IMPLEMENTED");
	}

	/**
	 * REST method
	 * 
	 * @return
	 * @throws IOException
	 */
	public Object post() throws IOException, ServletException {
		return notImplemented("NOT IMPLEMENTED");
	}

	/**
	 * REST method
	 * 
	 * @return
	 * @throws IOException
	 */
	public Object put() throws IOException, ServletException {
		return notImplemented("NOT IMPLEMENTED");
	}

	/**
	 * REST method
	 * 
	 * @return
	 * @throws IOException
	 */
	public Object trace() throws IOException, ServletException {
		return notImplemented("NOT IMPLEMENTED");
	}

	/**
	 * REST method
	 * 
	 * @return
	 * @throws IOException
	 * @throws ServletException
	 */
	public Object get() throws IOException, ServletException {
		return notImplemented("NOT IMPLEMENTED");
	}

	/**
	 * REST method
	 * 
	 * @return
	 * @throws IOException
	 */
	public Object delete() throws IOException {
		return notImplemented("NOT IMPLEMENTED");
	}

	/**
	 * Legge il body della richiesta, trasforma la stringa json in un oggetto
	 * 
	 * @return oggetto json
	 * @throws IOException
	 * @throws JsonSyntaxException
	 * @throws JsonIOException
	 */
	protected final JsonElement readRequestBodyAsJsonElement() throws JsonIOException, JsonSyntaxException, IOException {

		JsonParser parser = new JsonParser();

		return parser.parse(readRequestBodyAsInputStream());
	}

	/**
	 * Recupera il corpo della richiesta come stringa
	 * 
	 * @return
	 * @throws IOException
	 */
	protected final String readRequestBodyAsString() throws IOException {
		String payload = IOUtils.toString(readRequestBodyAsInputStream());

		log.info("payload: " + payload);

		return payload;
	}

	/**
	 * Apre lo stream sul body della richiesta
	 * 
	 * @return lo stream
	 * @throws IOException
	 */
	protected final InputStreamReader readRequestBodyAsInputStream() throws IOException {
		return new InputStreamReader(this.req.getInputStream());
	}

	// /**
	// * Legge il body della richiesta, trasforma la stringa in un tipo di oggetto scelto
	// *
	// * @param <T>
	// * @param classOf
	// * @return
	// */
	// protected final <T> T loadGSONRequestBody(Class<T> classOf) {
	// return gson.fromJson(loadInputStreamRequestBody(), classOf);
	// }

	/**
	 * Utilizza TypeToken
	 * 
	 * @return
	 * @throws IOException
	 * @throws JsonSyntaxException
	 * @throws JsonIOException
	 */
	protected final <T> T readRequestBody() throws JsonIOException, JsonSyntaxException, IOException {

		Type fooType = new TypeToken<T>() {
		}.getType();

		return gson.fromJson(readRequestBodyAsInputStream(), fooType);
	}

	/**
	 * usa this.gson.toJson(o)
	 * 
	 * @param o
	 * @return
	 * @throws IOException
	 */
	protected final <T> T ok(T o) throws IOException {
		this.writeString(this.gson.toJson(o), HttpServletResponse.SC_OK);

		return o;
	}

	/**
	 * utilizzare per oggetti della libreria Gson
	 * 
	 * @param o
	 * @return
	 * @throws IOException
	 */
	protected final JsonElement ok(JsonElement o) throws IOException {
		this.writeString(o.toString(), HttpServletResponse.SC_OK);

		return o;
	}

	/**
	 * utilizzarlo per i metodi non implementati risponde con stato 505
	 * 
	 * @param msg
	 * @return null
	 * @throws IOException
	 */
	protected final <T> T error(Object msg) throws IOException {
		JsonObject o = new JsonObject();
		o.add("msg", gson.toJsonTree(msg));

		this.writeString(o.toString(), HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

		return null;
	}

	protected final <T> T error(JsonElement msg) throws IOException {
		JsonObject o = new JsonObject();
		o.add("msg", msg);

		this.writeString(o.toString(), HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

		return null;
	}

	/**
	 * utilizzarlo per i metodi non implementati risponde con stato 501
	 * 
	 * @param msg
	 * @return null
	 * @throws IOException
	 */
	protected final <T> T notImplemented(Object msg) throws IOException {
		JsonObject o = new JsonObject();

		o.add("msg", gson.toJsonTree(msg));

		this.writeString(o.toString(), HttpServletResponse.SC_NOT_IMPLEMENTED);

		return null;
	}

	protected final <T> T notImplemented(JsonElement msg) throws IOException {
		JsonObject o = new JsonObject();

		o.add("msg", msg);

		this.writeString(o.toString(), HttpServletResponse.SC_NOT_IMPLEMENTED);

		return null;
	}

	/**
	 * risponde con 404
	 * 
	 * @param msg
	 * @return null
	 * @throws IOException
	 */
	protected final <T> T badRequest(Object msg) throws IOException {
		JsonObject o = new JsonObject();
		o.add("msg", gson.toJsonTree(msg));

		this.writeString(o.toString(), HttpServletResponse.SC_BAD_REQUEST);

		return null;
	}

	protected final <T> T badRequest(JsonElement msg) throws IOException {
		JsonObject o = new JsonObject();
		o.add("msg", msg);

		this.writeString(o.toString(), HttpServletResponse.SC_BAD_REQUEST);

		return null;
	}

	/**
	 * Scrive sulla response la stringa json passata e imposta lo stato http
	 * 
	 * @param jsonString
	 * @param httpStatus
	 * @return
	 * @throws IOException
	 */
	protected final String writeString(String jsonString, int httpStatus) throws IOException {

		String jsonpCallback = getJSONPCallback();

		String callback = (String) this.req.getParameter(jsonpCallback);

		if (callback != null) {
			/**
			 * JSONP
			 */

			String jsonP = callback + "( " + jsonString + " );";

			return this.write(JSONP_CONTENT_TYPE, getCharacterEncoding(), jsonP, httpStatus);

		} else {
			/**
			 * classic JSON
			 */

			return this.write(JSON_CONTENT_TYPE, getCharacterEncoding(), jsonString, httpStatus);

		}
	}

	/**
	 * Scrive sulla response la stringa data(vanno specificati anche parametri raw)
	 * 
	 * @param contentType
	 * @param characterEncoding
	 * @param response
	 * @param httpStatus
	 * @return
	 * @throws IOException
	 */
	protected final String write(String contentType, String characterEncoding, String response, int httpStatus) throws IOException {

		this.resp.setStatus(httpStatus);

		if (!isCached())
			this.resp.setHeader(CACHE_CONTROL_HEADER, CACHE_CONTROL_HEADER_VALUE);

		this.resp.setCharacterEncoding(characterEncoding);

		this.resp.setContentType(contentType);

		log.info(response);

		this.resp.getWriter().print(response);

		return response;
	}

	/**
	 * Inserisce nella coda di default un task su un rexy
	 * 
	 * @param <T>
	 * 
	 * @param rexy
	 * @param method
	 * @param payload
	 */
	public static final <T extends ArexMouth> void enqueue(Class<T> rexy, Method method, String payload) {

		Queue queue = QueueFactory.getDefaultQueue();

		String url = getRexyPath(rexy);

		queue.add(TaskOptions.Builder.withMethod(method).payload(payload).url(url));
	}

	/**
	 * Inserisce nella coda di default un task su un rexy
	 * 
	 * @param <T>
	 * @param rexy
	 * @param method
	 * @param to
	 */
	public static final <T extends ArexMouth> void enqueue(Class<T> rexy, Method method, TaskOptions to) {
		Queue queue = QueueFactory.getDefaultQueue();

		String url = getRexyPath(rexy);

		queue.add(to.method(method).url(url));
	}

	/**
	 * compone l'url per accedere al rexy specificatp
	 * 
	 * @param rexy
	 * @return
	 */
	public static final <T> String getRexyPath(Class<T> rexy) {
		return "/a/" + rexy.getName().replace(".", "/");
	}

	/**
	 * 
	 * @return
	 */
	public HttpServletRequest req() {
		return req;
	}

	/**
	 * 
	 * @param req
	 */
	public void setReq(HttpServletRequest req) {
		this.req = req;
	}

	/**
	 * 
	 * @return
	 */
	public HttpServletResponse resp() {
		return resp;
	}

	/**
	 * 
	 * @param resp
	 */
	public void setResp(HttpServletResponse resp) {
		this.resp = resp;
	}

	/**
	 * permette di fare il forward verso un altra classe che imp
	 * 
	 * @param rexy
	 * @return
	 * @return null
	 * @throws ServletException
	 * @throws IOException
	 */
	protected final <T extends ArexMouth> T forwardTo(T rexy) throws ServletException, IOException {

		String path = getRexyPath(rexy.getClass());
		log.info(path);

		req.setAttribute(ArexFilter.REXY_ATTRIBUTE, rexy);
		req.setAttribute(ArexFilter.ALIAS_ATTRIBUTE, null); // la richiesta va diretta sul controller arex, devo pulire questo campo

		servletContext().getRequestDispatcher(path).forward(req, resp);

		return rexy;
	}

	/**
	 * Utilizzare per fare il forward verso pagine JSP
	 * 
	 * @param jspPath
	 * @throws ServletException
	 * @throws IOException
	 */
	protected final <T> T forwardTo(String jspPath) throws ServletException, IOException {

		log.info(jspPath);

		servletContext().getRequestDispatcher(jspPath).forward(req, resp);

		return null;
	}

	/**
	 * redirect
	 * 
	 * @param uri
	 * @throws IOException
	 */
	protected final <T> T redirect(String uri) throws IOException {
		log.info(uri);

		resp.sendRedirect(uri);

		return null;
	}

	/**
	 * Consente di richiamare un altro rexy simulando una request HTTP, in questo modo è possibile riutilizzare metodi presenti in altri controller
	 * 
	 * @param rexy
	 * @param req
	 * @return null se ci sono problemi
	 * @throws IllegalArgumentException
	 * @throws InstantiationException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 */
	protected final <T extends ArexMouth> T call(Class<T> rexy, HttpServletRequest req) {

		try {
			Constructor<T> ct = rexy.getConstructor();

			T arex = ct.newInstance();

			arex.setReq(req);

			HttpServletResponse resp = new ArexVoidHttpResp();

			arex.setResp(resp);

			return arex;
		} catch (IllegalArgumentException e) {
			log.severe("IllegalArgumentException: " + e.getMessage());
		} catch (InstantiationException e) {
			log.severe("InstantiationException: " + e.getMessage());
		} catch (IllegalAccessException e) {
			log.severe("IllegalAccessException: " + e.getMessage());
		} catch (InvocationTargetException e) {
			log.severe("InvocationTargetException: " + e.getMessage());
		} catch (SecurityException e) {
			log.severe("SecurityException: " + e.getMessage());
		} catch (NoSuchMethodException e) {
			log.severe("NoSuchMethodException: " + e.getMessage());
		}

		return null;
	}

	protected final Boolean acceptJSON() {
		return req.getHeader(ACCEPT_HEADER).contains(JSON_CONTENT_TYPE);
	}

	public void setServletContext(ServletContext servletContext) {
		this.servletContext = servletContext;
	}

	public ServletContext servletContext() {
		return servletContext;
	}

	@Override
	public Object get(String id) throws IOException, ServletException {
		return notImplemented("NOT IMPLEMENTED");
	}

	@Override
	public Object search(String q) throws IOException, ServletException {
		return notImplemented("NOT IMPLEMENTED");
	}

	@Override
	public Object post(String id) throws IOException, ServletException {
		return notImplemented("NOT IMPLEMENTED");
	}

	@Override
	public Object put(String id) throws IOException, ServletException {
		return notImplemented("NOT IMPLEMENTED");
	}

	@Override
	public Object delete(String id) throws IOException, ServletException {
		return notImplemented("NOT IMPLEMENTED");
	}
}